﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Web.Compilation;
using System.Reflection;
using System.Linq;
using System.Web.UI.WebControls;
using System.Web.Mvc;
using System.Web.UI;
using System.Web;
using System.IO;

namespace Kona.Infrastructure {

    public class Plugin {
        // TODO: ensure thread safety.
        // TODO: make sure the scope of the cache here is small enough.
        private static List<Plugin> _plugins = new List<Plugin>();
        private static Hashtable _methods = Hashtable.Synchronized(new Hashtable());
        private static Hashtable _cached = Hashtable.Synchronized(new Hashtable());
        // InitializePlugins needs to be reentrant
        private static bool s_inited;
        private static object s_initLock = new object();
        private static IList<PluginSetting> _settings;

        //settings
        PluginSetting _setting;
        public PluginSetting Settings {
            get {
                _setting=Plugin._settings.SingleOrDefault(x => x.PluginName.Equals(this.PluginName, StringComparison.InvariantCultureIgnoreCase));
                return _setting ?? new PluginSetting();
            }
        }
        
        public bool IsEnabled {
            get {
                bool result = false;
                //the plugin is enabled if
                //1) the settings have been filled in
                //2) and the user says it is
                if (this.Settings.IsEnabled) {
                    //make sure each setting is set
                    result= this.Settings.IsEnabled;

                    //TODO: Figure out a check for required values


                }

                return result;
            }
        
        }
        public string PluginName {
            get {
                return this.GetType().Name;
            }
        }
        public string Code {
            get {
                return FilePath.GetFileText();
            }

        }
        string _filePath = "";
        public string FilePath {
            get {
                if (string.IsNullOrEmpty(_filePath)) {
                    string directoryRoot = HttpContext.Current.Server.MapPath("~/App_Code");
                    _filePath=this.GetType().Name.LocateFilePath(directoryRoot);
                }
                return _filePath;
            }
            set {
                _filePath = value;
            }
        }

        public static void LoadSettings() {
            ObjectStore store = new ObjectStore();
            _settings=store.GetList<PluginSetting>("PluginSetting");
            
        }

        public static void ValidateSetting(string pluginName, string settingName, object value) {
            var plugin = Plugins.Where(x => x.GetType().Name.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)).SingleOrDefault();
            if (plugin != null) {

                PropertyInfo prop = plugin.GetType().GetProperty(settingName);

                //see if it can be assigned this value
                if (prop != null) {
                    //set the value to see if it can be coerced
                    try {
                        value.ChangeType(prop.PropertyType);
                    } catch {
                        throw new InvalidOperationException("Can't assign " + value.ToString() + " to " + prop.PropertyType.Name);
                    }
                } else {
                    throw new InvalidOperationException("No setting found to validate with that name");
                }

            } else {
                throw new InvalidOperationException("No plugin found to validate with that name");
            }
        }

        /// <summary>
        /// Optionally override this in a plug-in to add an initialization step.
        /// </summary>
        public virtual void Initialize() { }


        //initializes based on App_Code
        public static IList<Plugin> InitializePlugins() {
            var codeAssemblies = System.Web.Compilation.BuildManager.CodeAssemblies;
            return InitializePlugins(codeAssemblies);
        }

        public object GetSetting(string name) {
            //see if this setting is presetn
            object result = null;
            if (this.Settings.Settings.ContainsKey(name))
                result = this.Settings.Settings[name];

            return result;
        }
        

        //TODO: Fix this to be less arrow-y
        /// <summary>
        /// Discovers and initializes the plug-ins.
        /// </summary>
        public static IList<Plugin> InitializePlugins(IList codeAssemblies) {
            if (s_inited)
                return null;
            LoadSettings();
            lock (s_initLock) {
                if (!s_inited) {
                    if (codeAssemblies != null) {
                        foreach (Assembly assembly in codeAssemblies) {
                            if (!assembly.FullName.StartsWith("System") & !assembly.FullName.StartsWith("Microsoft.VisualStudio")) {
                                try {
                                    foreach (var type in assembly.GetTypes()) {
                                        if (typeof(Plugin).IsAssignableFrom(type) && type != typeof(Plugin)) {
                                            var plugin = (Plugin)Activator.CreateInstance(type);
                                            plugin.Initialize();
                                            _plugins.Add(plugin);
                                        }
                                    }
                                }
                                catch (Exception x) {
                                    if (x is System.Reflection.ReflectionTypeLoadException) {
                                        var newX = x as System.Reflection.ReflectionTypeLoadException;
                                        throw new InvalidOperationException(newX.LoaderExceptions[0].Message);
                                    }
                                    else {
                                        throw;
                                    }
                                }
                            }
                        }
                    }
                    s_inited = true;
                }
            }
            return _plugins;
        }

        /// <summary>
        /// The list of installed plug-ins.
        /// </summary>
        public static Plugin[] Plugins {
            get {
                return _plugins.ToArray();
            }
        }
        public static TResult Execute<TResult>(string pluginName, string operation, params object[] input) {

            MethodInfo method = null;
            TResult result = default(TResult);

            //find the plugin
            var plugin = _plugins.Where(x => x.GetType().Name.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)).SingleOrDefault();

            if (plugin != null) {
                method = plugin.GetType().GetMethods().FirstOrDefault(x => x.Name == operation);

                if (method != null) {
                    result = (TResult)method.Invoke(plugin, input);
                }
                //load the first
            }
            return result;
        }

        public static TResult ExecuteFirst<TResult>(string operation, params object[] input) {

            MethodInfo method = null;
            Plugin foundPlugin=null;
            TResult result = default(TResult);
            foreach (var plugin in _plugins) {
                method = plugin.GetType().GetMethods().FirstOrDefault(x => x.Name == operation);
                if (method != null){
                    foundPlugin=plugin;
                    break;
                }
            }

            if (method != null) {
                result = (TResult)method.Invoke(foundPlugin, input);
            
            }
            //load the first

            return result;

        }

        /// <summary>
        /// Gets a method that calls all the plug-ins of the provided signature.
        /// </summary>
        /// <typeparam name="T">A delegate type describing the expected signature.</typeparam>
        /// <returns>A method that calls all the plug-ins with the signature of T.</returns>
        public static T GetMethod<T>(string operation) where T : class {
            var operationMethods = _methods[operation] as Hashtable;
            var operationCached = _cached[operation] as Hashtable;
            if (operationMethods == null) {
                operationMethods = Hashtable.Synchronized(new Hashtable());
                _methods.Add(operation, operationMethods);
                operationCached = Hashtable.Synchronized(new Hashtable());
                _cached.Add(operation, operationCached);
            }
            var processDelegate = (Delegate)operationMethods[typeof(T)];
            if (processDelegate == null) {
                if (operationCached[typeof(T)] == null) {
                    foreach (var plugin in _plugins) {
                        var pluginType = plugin.GetType();
                        var processMethod = pluginType.GetMethod(operation,
                            BindingFlags.Instance | BindingFlags.Public | BindingFlags.IgnoreCase);
                        if (processMethod == null) continue;
                        var newDelegate = Delegate.CreateDelegate(typeof(T), plugin, processMethod, false);
                        if (newDelegate != null) {
                            if (processDelegate == null) {
                                processDelegate = newDelegate;
                            }
                            else {
                                processDelegate = Delegate.Combine(processDelegate, newDelegate);
                            }
                        }
                    }
                    operationMethods[typeof(T)] = processDelegate;
                    operationCached[typeof(T)] = true;
                }
            }
            return (T)(object)processDelegate;
        }

        /// <summary>
        /// Feeds the provided input through all plug-ins that have T [operation](string, T)
        /// as their signature, where [operation] is the contents of the operation parameter.
        /// </summary>
        /// <typeparam name="T">The type of the input to process.</typeparam>
        /// <param name="operation">The key of the operation to perform.</param>
        /// <param name="input">The object to process.</param>
        /// <returns>The processed object.</returns>
        public static T Process<T>(string operation, T input) where T : class {
            var methods = GetMethod<Func<T, T>>(operation);
            if (methods != null) {
                foreach (Func<T, T> pluginMethod in methods.GetInvocationList()) {
                    if (input == null) return null;
                    input = pluginMethod(input);
                }
            }
            return input;
        }

        /// <summary>
        /// Puts the provided object through all plug-ins with signature
        /// bool Process(string, T). The return value is false if any of the
        /// plug-ins returned false, and true otherwise. This is the pattern
        /// to use for pluggable validation logic for example.
        /// </summary>
        /// <typeparam name="T">The type of the input to process.</typeparam>
        /// <param name="operation">The key of the operation to perform.</param>
        /// <param name="input">The object to process.</param>
        /// <returns>False if any of the plug-ins returned false.</returns>
        public static bool IsTrue<T>(string operation, T input) {
            Delegate methods = GetMethod<Func<T, bool>>(operation);
            if (methods == null) return true;
            foreach (var pluginMethod in methods.GetInvocationList()) {
                if (!(bool)pluginMethod.DynamicInvoke(input)) {
                    return false;
                }
            }
            return true;
        }


    }
}
